iT邦幫忙

2023 iThome 鐵人賽

DAY 22
0

今天要來學習的 Reducer 的應用,主要內容為 Reducer 的操作步驟。

較複雜的事件處理器

隨著程式越來越複雜,我們可能會遇到下面這樣的情況

像是在這裡,需要處理三種事件:「追加項目」、「刪除項目」、「編輯」。

import { useState } from "react";
import AddTask from "./AddTask.js";
import TaskList from "./TaskList.js";

export default function TaskApp() {
  const [tasks, setTasks] = useState(initialTasks);

  function handleAddTask(text) {
    setTasks([
      ...tasks,
      {
        id: nextId++,
        text: text,
        done: false,
      },
    ]);
  }

  function handleChangeTask(task) {
    setTasks(
      tasks.map((t) => {
        if (t.id === task.id) {
          return task;
        } else {
          return t;
        }
      })
    );
  }

  function handleDeleteTask(taskId) {
    setTasks(tasks.filter((t) => t.id !== taskId));
  }

  return (
    <>
      <h1>Prague itinerary</h1>
      <AddTask onAddTask={handleAddTask} />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

let nextId = 3;
const initialTasks = [
  { id: 0, text: "Visit Kafka Museum", done: true },
  { id: 1, text: "Watch a puppet show", done: false },
  { id: 2, text: "Lennon Wall pic", done: false },
];

若想要統一管理這樣的 event handlers,可以使用 Reducer 來操作,一共用三個步驟。

Step 1:從設定狀態(state setting)轉為派發動作(dispatch action)

這邊要做的事情是改變 event handlers 的內容,先將 state 相關的邏輯移除(這個後續會再處理)

,留下 event handlers 更純粹要操作事情,也就是告訴 React 使用者正在操作什麼事情,例如「新增了一個任務」、「更改了一個任務」或「刪除了一個任務」。實際來看看程式碼:

  • Before:
function handleAddTask(text) {
  setTasks([
    ...tasks,
    {
      id: nextId++,
      text: text,
      done: false,
    },
  ]);
}
  • After:
function handleAddTask(text) {
  dispatch({
    type: "added",
    id: nextId++,
    text: text,
  });
}

我們使用了 dispatch 去做這件事,可以看見 dispatch 內部傳入了一個 object,這個 object 稱為「action object」。

「action object」的內部通常可以在type以 string 來說明發生什麼事,其他可以按需求傳入額外的資訊即可。像是這樣子:

dispatch({
  // 描述發生什麼事情
  type: "what_happened",
  // 其他額外資訊
});

Step 2 : 撰寫 reducer function

在 Reducer function 我們要使用剛剛的 action object 和先前拿掉的 state 的邏輯。

function yourReducer(state, action) {
  // return next state for React to set
}

Reducer function 會有二個參數,一個是 state 的邏輯,另一個是 action object。先讓我們使用 if/else statement 來寫寫看:

function tasksReducer(tasks, action) {
  if (action.type === "added") {
    return [
      ...tasks,
      {
        id: action.id,
        text: action.text,
        done: false,
      },
    ];
  } else if (action.type === "changed") {
    return tasks.map((t) => {
      if (t.id === action.task.id) {
        return action.task;
      } else {
        return t;
      }
    });
  } else if (action.type === "deleted") {
    return tasks.filter((t) => t.id !== action.id);
  } else {
    throw Error("Unknown action: " + action.type);
  }
}

像以上這樣透過判定 action object 的 type (也就是第一步的要操作什麼事情)來決定回傳的東西。要留意這邊回傳的會是「下一個 state」(next state)。另外,這邊雖然是使用 if/else statement 來操作,在通常的情況下多為使用 switch statement。這樣子就完成第二步的操作了。

Step 3:在元件中使用 reducer

最後要來把剛剛學的結合起來。

首先需要先 import useReducer

import { useReducer } from "react";

再來,將useState取代為useReducer

const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

這邊需要留意的是和僅需要初始值的useState不同useReducer需要「reducer function」和「初始的 state(initial state)」二個 arguments。而它 return 一個「狀態值(stateful value)」和「dispatch function」。

回顧一下,狀態值(stateful value)是什麼?

狀態值是表示應用程式當前狀態的一個值。它代表了應用程式的數據或狀態的快照。而在目前的例子 return 的 dispatch 用於觸發列表的更新,並會在 event handlers 當中被呼叫。

將全部結合起來就會是以下的程式碼:

import { useReducer } from "react";
import AddTask from "./AddTask.js";
import TaskList from "./TaskList.js";

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

  function handleAddTask(text) {
    dispatch({
      type: "added",
      id: nextId++,
      text: text,
    });
  }

  function handleChangeTask(task) {
    dispatch({
      type: "changed",
      task: task,
    });
  }

  function handleDeleteTask(taskId) {
    dispatch({
      type: "deleted",
      id: taskId,
    });
  }

  return (
    <>
      <h1>Prague itinerary</h1>
      <AddTask onAddTask={handleAddTask} />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

function tasksReducer(tasks, action) {
  switch (action.type) {
    case "added": {
      return [
        ...tasks,
        {
          id: action.id,
          text: action.text,
          done: false,
        },
      ];
    }
    case "changed": {
      return tasks.map((t) => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case "deleted": {
      return tasks.filter((t) => t.id !== action.id);
    }
    default: {
      throw Error("Unknown action: " + action.type);
    }
  }
}

let nextId = 3;
const initialTasks = [
  { id: 0, text: "Visit Kafka Museum", done: true },
  { id: 1, text: "Watch a puppet show", done: false },
  { id: 2, text: "Lennon Wall pic", done: false },
];

useState vs useReducer

讓我們比較看看useStateuseReducer

方面 useState useReducer
程式碼大小 需要較少的程式碼 需要定義 reducer 函數和派發 actions
可讀性 當狀態更新簡單時容易閱讀 當狀態更新複雜時,更容易分離更新邏輯
調試 難以找到狀態設置錯誤的位置和原因 可以在 reducer 中添加 console log 以更清晰地追蹤狀態更新和原因
測試 需要在元件內測試 可以將 reducer 單獨測試

至於要選用 useState 還是 useReducer,這個可以依據專案的需求或是偏好而定。

參考資料

  • React 官方文件 - Extracting State Logic into a Reducer

上一篇
Day 21 - 保存和重置 state
下一篇
Day 23 - prop drilling 和 context
系列文
30 days of React 30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言